package dal

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"

	"gitee.com/lipore/plume/db_gorm/dal/models"
	"gitee.com/lipore/plume/db_gorm/dal/parser"
	"golang.org/x/tools/imports"

	tmpl "gitee.com/lipore/plume/db_gorm/dal/template"
)

type Generator struct {
	modelPath  string //interface definition
	entityPath string //entity implement
	modelPkg   *models.Package
	entityPkg  *models.Package
}

func NewGenerator(modelPath, entityPath string) *Generator {
	return &Generator{
		modelPath:  modelPath,
		entityPath: entityPath,
	}
}

func (g *Generator) Generate() {
	removeFiles(g.entityPath, func(name string) bool {
		return strings.HasSuffix(name, ".dal.go")
	})
	removeFiles(g.modelPath, func(name string) bool {
		return strings.HasSuffix(name, ".dal.go")
	})
	modelPkg, err := parser.ParsePackage(g.modelPath)
	if err != nil {
		panic(err)
	}
	entityPkg, err := parser.ParsePackage(g.entityPath)
	if err != nil {
		panic(err)
	}
	g.modelPkg = modelPkg[0]
	g.entityPkg = entityPkg[0]

	for _, entity := range g.entityPkg.StructInfos {
		if entity.Annotations.Len() > 0 {
			modelName := entity.ModelName()

			var diyDoInterface *models.InterfaceInfo
			for _, intf := range g.modelPkg.InterfaceInfos {
				if intf.ModelName() == modelName {
					diyDoInterface = intf
					break
				}
			}
			entityInfo := models.NewEntityInfo(entity, diyDoInterface, g.modelPkg)
			g.GenerateDoInterfaceFile(modelName, entityInfo)
			g.GenerateDoStructFile(modelName, entityInfo)
		}
	}
}

func formatAndWriteFile(path string, content []byte) *os.File {
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	formattedCode, err := imports.Process("", content, nil)
	if err != nil {
		fmt.Println(string(content))
		panic(err)
	}
	f.Write(formattedCode)
	return f
}

func removeFiles(dir string, filter func(string) bool) {
	f, err := os.Open(dir)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	names, err := f.Readdirnames(-1)
	if err != nil {
		panic(err)
	}
	for _, name := range names {
		if filter(name) {
			err = os.Remove(dir + "/" + name)
			if err != nil {
				panic(err)
			}
		}
	}
}

func (g *Generator) GenerateDoInterfaceFile(
	modelName string,
	entityInfo *models.EntityInfo,
) *models.EntityInfo {

	buffer := new(bytes.Buffer)
	tmpl.GenerateHeader(buffer, &tmpl.HdaderData{
		Package: g.modelPkg.Name,
	})
	// tmpl.GenerateDoInterface(buffer, entityInfo.DalInterface)

	containerData := &tmpl.DoCreatorData{
		ModelName:      modelName,
		ModelInterface: entityInfo.DalInterface.Name,
	}
	tmpl.GenerateDoCreator(buffer, containerData)

	formatAndWriteFile(g.modelPath+"/"+strings.ToLower(modelName)+".dal.go", buffer.Bytes())
	return entityInfo
}

func (g *Generator) GenerateDoStructFile(modelName string, entityInfo *models.EntityInfo) {
	buffer := new(bytes.Buffer)
	tmpl.GenerateHeader(buffer, &tmpl.HdaderData{
		Package: g.entityPkg.Name,
		Imports: []string{
			"context",
			g.modelPkg.Path,
			"gitee.com/lipore/plume/db_gorm",
		},
	})

	tmpl.GenerateEntityCreator(buffer, &tmpl.EntityCreatorData{
		ModelName:      modelName,
		ModelInterface: entityInfo.DalInterface.FullName(),
		ModelPkgName:   entityInfo.DalInterface.Package.Name,
		EntityName:     entityInfo.DalStruct.Name,
	})

	NewEntityGenerator(entityInfo, g).Generate(buffer, modelName)

	path := g.entityPath + "/" + strings.ToLower(entityInfo.DalStruct.Name) + ".dal.go"
	formatAndWriteFile(path, buffer.Bytes())
}

type EntityGenerator struct {
	entityInfo *models.EntityInfo
	g          *Generator
}

func NewEntityGenerator(
	entityInfo *models.EntityInfo,
	g *Generator,
) *EntityGenerator {
	return &EntityGenerator{
		entityInfo: entityInfo,
		g:          g,
	}
}

func (b *EntityGenerator) render(buffer *bytes.Buffer, name string, creator func(io.Writer, interface{}), data interface{}) {
	if im, exist := b.entityInfo.DalInterface.GetMethod(name); exist {
		if m, exist := b.entityInfo.DalStruct.GetMethod(name); exist {
			if !m.Equal(im) {
				panic(name + " method is implemented with different signature")
			} else {
				return
			}
		}
		creator(buffer, data)
		b.entityInfo.DalStruct.Methods = append(b.entityInfo.DalStruct.Methods, im)
	} else {
		panic(name + " method is not found in interface")
	}
}

func (b *EntityGenerator) Generate(buffer *bytes.Buffer, modelName string) {
	for _, field := range b.entityInfo.DalStruct.Fields {
		getSetData := tmpl.GetSetData{
			EntityName: b.entityInfo.DalStruct.Name,
			FieldName:  field.Name,
			FieldType:  field.Type,
		}
		b.render(buffer, "Set"+field.Name, tmpl.GenerateSet, getSetData)
		b.render(buffer, "Get"+field.Name, tmpl.GenerateGet, getSetData)
	}

	bcData := &tmpl.BaseCrudData{
		EntityName: b.entityInfo.DalStruct.Name,
		IdFields:   b.entityInfo.IdFields,
	}
	b.render(buffer, "Create", tmpl.GenerateCreate, bcData)
	b.render(buffer, "Find", tmpl.GenerateFind, bcData)
	b.render(buffer, "Update", tmpl.GenerateUpdate, bcData)
	b.render(buffer, "Delete", tmpl.GenerateDelete, bcData)

	if b.entityInfo.DalDIYDoInterface != nil {
		for _, method := range b.entityInfo.DalDIYDoInterface.Methods {
			if _, exist := b.entityInfo.DalStruct.GetMethod(method.Name); !exist {
				b.render(buffer, method.Name, tmpl.GenerateNotImplement, &tmpl.NotImplementData{
					EntityName: b.entityInfo.DalStruct.Name,
					Method:     method,
				})
			}
		}
	}
}

func generateFindMethod(buffer *bytes.Buffer, m *models.MethodInfo) {
	name := m.Name
	nameParttern := regexp.MustCompile(`Find(\w*)By(\w*)`)
	matches := nameParttern.FindStringSubmatch(name)
	if len(matches) != 3 {
		panic("invalid find method name")
	}
	// fields := matches[1]
	// conditions := matches[2]
}
